/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactUpdates
 */

'use strict'

var _assign = require('object-assign')

var CallbackQueue = require('./CallbackQueue')
var PooledClass = require('./PooledClass')
var ReactFeatureFlags = require('./ReactFeatureFlags')
var ReactPerf = require('./ReactPerf')
var ReactReconciler = require('./ReactReconciler')
var Transaction = require('./Transaction')

var invariant = require('fbjs/lib/invariant')

var dirtyComponents = []
var asapCallbackQueue = CallbackQueue.getPooled()
var asapEnqueued = false

var batchingStrategy = null

function ensureInjected() {
  !(ReactUpdates.ReactReconcileTransaction && batchingStrategy)
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          'ReactUpdates: must inject a reconcile transaction class and batching ' +
            'strategy'
        )
      : invariant(false)
    : void 0
}

var NESTED_UPDATES = {
  initialize: function () {
    this.dirtyComponentsLength = dirtyComponents.length
  },
  close: function () {
    if (this.dirtyComponentsLength !== dirtyComponents.length) {
      // Additional updates were enqueued by componentDidUpdate handlers or
      // similar; before our own UPDATE_QUEUEING wrapper closes, we want to run
      // these new updates so that if A's componentDidUpdate calls setState on
      // B, B will update before the callback A's updater provided when calling
      // setState.
      dirtyComponents.splice(0, this.dirtyComponentsLength)
      flushBatchedUpdates()
    } else {
      dirtyComponents.length = 0
    }
  }
}

var UPDATE_QUEUEING = {
  initialize: function () {
    this.callbackQueue.reset()
  },
  close: function () {
    this.callbackQueue.notifyAll()
  }
}

var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING]

function ReactUpdatesFlushTransaction() {
  this.reinitializeTransaction()
  this.dirtyComponentsLength = null
  this.callbackQueue = CallbackQueue.getPooled()
  this.reconcileTransaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    /* useCreateElement */ true
  )
}

_assign(ReactUpdatesFlushTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS
  },

  destructor: function () {
    this.dirtyComponentsLength = null
    CallbackQueue.release(this.callbackQueue)
    this.callbackQueue = null
    ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction)
    this.reconcileTransaction = null
  },

  perform: function (method, scope, a) {
    // Essentially calls `this.reconcileTransaction.perform(method, scope, a)`
    // with this transaction's wrappers around it.
    return Transaction.Mixin.perform.call(
      this,
      this.reconcileTransaction.perform,
      this.reconcileTransaction,
      method,
      scope,
      a
    )
  }
})

PooledClass.addPoolingTo(ReactUpdatesFlushTransaction)

function batchedUpdates(callback, a, b, c, d, e) {
  ensureInjected()
  batchingStrategy.batchedUpdates(callback, a, b, c, d, e)
}

/**
 * Array comparator for ReactComponents by mount ordering.
 *
 * @param {ReactComponent} c1 first component you're comparing
 * @param {ReactComponent} c2 second component you're comparing
 * @return {number} Return value usable by Array.prototype.sort().
 */
function mountOrderComparator(c1, c2) {
  return c1._mountOrder - c2._mountOrder
}

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength
  !(len === dirtyComponents.length)
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          "Expected flush transaction's stored dirty-components length (%s) to " +
            'match dirty-components array length (%s).',
          len,
          dirtyComponents.length
        )
      : invariant(false)
    : void 0

  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator)

  for (var i = 0; i < len; i++) {
    // If a component is unmounted before pending changes apply, it will still
    // be here, but we assume that it has cleared its _pendingCallbacks and
    // that performUpdateIfNecessary is a noop.
    var component = dirtyComponents[i]

    // If performUpdateIfNecessary happens to enqueue any new updates, we
    // shouldn't execute the callbacks until the next render happens, so
    // stash the callbacks first
    var callbacks = component._pendingCallbacks
    component._pendingCallbacks = null

    var markerName
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component
      // Duck type TopLevelWrapper. This is probably always true.
      if (
        component._currentElement.props ===
        component._renderedComponent._currentElement
      ) {
        namedComponent = component._renderedComponent
      }
      markerName = 'React update: ' + namedComponent.getName()
      console.time(markerName)
    }

    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction
    )

    if (markerName) {
      console.timeEnd(markerName)
    }

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance()
        )
      }
    }
  }
}

var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled()
      transaction.perform(runBatchedUpdates, null, transaction)
      ReactUpdatesFlushTransaction.release(transaction)
    }

    if (asapEnqueued) {
      asapEnqueued = false
      var queue = asapCallbackQueue
      asapCallbackQueue = CallbackQueue.getPooled()
      queue.notifyAll()
      CallbackQueue.release(queue)
    }
  }
}
flushBatchedUpdates = ReactPerf.measure(
  'ReactUpdates',
  'flushBatchedUpdates',
  flushBatchedUpdates
)

/**
 * Mark a component as needing a rerender, adding an optional callback to a
 * list of functions which will be executed once the rerender occurs.
 */
function enqueueUpdate(component) {
  ensureInjected()

  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setProps, setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component)
    return
  }

  dirtyComponents.push(component)
}

/**
 * Enqueue a callback to be run at the end of the current batching cycle. Throws
 * if no updates are currently being performed.
 */
function asap(callback, context) {
  !batchingStrategy.isBatchingUpdates
    ? process.env.NODE_ENV !== 'production'
      ? invariant(
          false,
          "ReactUpdates.asap: Can't enqueue an asap callback in a context where" +
            'updates are not being batched.'
        )
      : invariant(false)
    : void 0
  asapCallbackQueue.enqueue(callback, context)
  asapEnqueued = true
}

var ReactUpdatesInjection = {
  injectReconcileTransaction: function (ReconcileTransaction) {
    !ReconcileTransaction
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'ReactUpdates: must provide a reconcile transaction class'
          )
        : invariant(false)
      : void 0
    ReactUpdates.ReactReconcileTransaction = ReconcileTransaction
  },

  injectBatchingStrategy: function (_batchingStrategy) {
    !_batchingStrategy
      ? process.env.NODE_ENV !== 'production'
        ? invariant(false, 'ReactUpdates: must provide a batching strategy')
        : invariant(false)
      : void 0
    !(typeof _batchingStrategy.batchedUpdates === 'function')
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'ReactUpdates: must provide a batchedUpdates() function'
          )
        : invariant(false)
      : void 0
    !(typeof _batchingStrategy.isBatchingUpdates === 'boolean')
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            'ReactUpdates: must provide an isBatchingUpdates boolean attribute'
          )
        : invariant(false)
      : void 0
    batchingStrategy = _batchingStrategy
  }
}

var ReactUpdates = {
  /**
   * React references `ReactReconcileTransaction` using this property in order
   * to allow dependency injection.
   *
   * @internal
   */
  ReactReconcileTransaction: null,

  batchedUpdates: batchedUpdates,
  enqueueUpdate: enqueueUpdate,
  flushBatchedUpdates: flushBatchedUpdates,
  injection: ReactUpdatesInjection,
  asap: asap
}

module.exports = ReactUpdates
